iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Rust

30天Rust從零到全端系列 第 9

Day 9: 借用與參考:不轉移所有權

  • 分享至 

  • xImage
  •  

前言

昨天我們深入學習了所有權系統,你可能已經發現一個問題:如果每次使用資料都要轉移所有權,那程式設計會變得非常麻煩。今天,我們要學習 Rust 的另一個核心概念 - 借用(Borrowing)與參考(References),這是讓所有權系統變得實用的關鍵。

從昨天的問題開始

還記得昨天的任務管理系統嗎?我們必須不斷地返回所有權才能繼續使用資料:

// 昨天的麻煩寫法
fn main() {
    let task = create_task();
    let task = process_task(task);  // 必須取回所有權
    let task = update_task(task);   // 又要取回一次
    let task = complete_task(task); // 再取回一次...
    
    println!("最終狀態: {:?}", task);
}

這樣寫太累人了!讓我們用借用來解決這個問題。

參考的基本概念

參考(Reference)允許你使用值但不擁有它:

fn main() {
    let s1 = String::from("hello");
    
    let len = calculate_length(&s1);  // &s1 創建一個參考
    
    println!("'{}' 的長度是 {}", s1, len);  // s1 仍然有效!
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 離開作用域,但因為它不擁有值,所以什麼都不會發生

視覺化參考

堆疊(Stack)              堆積(Heap)
┌─────────┐              ┌─────────────┐
│   s1    │ ──────────>  │  "hello"    │
├─────────┤              └─────────────┘
│   &s1   │ ──────────────────┘
└─────────┘     (參考指向同一個值)

借用的規則

Rust 對借用有嚴格的規則,這些規則在編譯時就會檢查:

規則 1:不可變參考可以有多個

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;  // 沒問題
    let r2 = &s;  // 沒問題
    let r3 = &s;  // 沒問題
    
    println!("{}, {}, {}", r1, r2, r3);
}

規則 2:可變參考只能有一個

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    // let r2 = &mut s;  // 錯誤!不能有兩個可變參考
    
    r1.push_str(", world");
    println!("{}", r1);
}

規則 3:不能同時有可變和不可變參考

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 不可變參考
    let r2 = &s;      // 另一個不可變參考
    // let r3 = &mut s;  // 錯誤!不能同時有可變參考
    
    println!("{} and {}", r1, r2);
    // r1 和 r2 的作用域在這裡結束
    
    let r3 = &mut s;  // 現在可以創建可變參考了
    r3.push_str(", world");
    println!("{}", r3);
}

參考的作用域

Rust 2018 引入了 NLL(Non-Lexical Lifetimes),讓參考的作用域更智能:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // r1 和 r2 在這裡之後不再使用,作用域結束
    
    let r3 = &mut s;  // 可以創建可變參考了
    r3.push_str(", world");
    println!("{}", r3);
}

實戰:改進任務管理系統

使用不可變參考

#[derive(Debug)]
struct Task {
    title: String,
    description: String,
    completed: bool,
}

impl Task {
    fn new(title: String, description: String) -> Self {
        Task {
            title,
            description,
            completed: false,
        }
    }
    
    // 使用 &self - 不可變借用
    fn display(&self) {
        println!("任務: {}", self.title);
        println!("描述: {}", self.description);
        println!("狀態: {}", if self.completed { "已完成" } else { "進行中" });
    }
    
    // 取得資訊但不修改
    fn get_title(&self) -> &str {
        &self.title
    }
    
    fn is_completed(&self) -> bool {
        self.completed
    }
}

fn main() {
    let task = Task::new(
        String::from("學習借用"),
        String::from("理解 Rust 的借用機制")
    );
    
    task.display();  // 借用 task
    
    let title = task.get_title();  // 再次借用
    println!("任務標題: {}", title);
    
    // task 仍然可用!
    println!("任務狀態: {:?}", task);
}

使用可變參考

impl Task {
    // 使用 &mut self - 可變借用
    fn complete(&mut self) {
        self.completed = true;
        println!("任務 '{}' 已完成!", self.title);
    }
    
    fn update_description(&mut self, new_desc: String) {
        self.description = new_desc;
    }
    
    fn add_note(&mut self, note: &str) {
        self.description.push_str("\n備註: ");
        self.description.push_str(note);
    }
}

fn main() {
    let mut task = Task::new(
        String::from("學習借用"),
        String::from("理解 Rust 的借用機制")
    );
    
    task.add_note("這很重要!");  // 可變借用
    task.complete();               // 另一個可變借用
    
    task.display();  // 不可變借用
}

切片(Slices)- 另一種參考

切片讓你參考集合的一部分:

字串切片

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5];   // 或 &s[..5]
    let world = &s[6..11];  // 或 &s[6..]
    let whole = &s[..];     // 整個字串的切片
    
    println!("{} {}", hello, world);
}

// 更實用的例子
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    &s[..]
}

陣列切片

fn main() {
    let a = [1, 2, 3, 4, 5];
    
    let slice = &a[1..3];  // [2, 3]
    
    assert_eq!(slice, &[2, 3]);
    
    // 使用切片的函式
    let sum = sum_slice(slice);
    println!("總和: {}", sum);
}

fn sum_slice(slice: &[i32]) -> i32 {
    let mut sum = 0;
    for &item in slice {
        sum += item;
    }
    sum
}

進階模式:多重借用

struct TaskManager {
    tasks: Vec<Task>,
}

impl TaskManager {
    fn new() -> Self {
        TaskManager { tasks: Vec::new() }
    }
    
    fn add_task(&mut self, task: Task) {
        self.tasks.push(task);
    }
    
    // 返回不可變參考
    fn get_task(&self, index: usize) -> Option<&Task> {
        self.tasks.get(index)
    }
    
    // 返回可變參考
    fn get_task_mut(&mut self, index: usize) -> Option<&mut Task> {
        self.tasks.get_mut(index)
    }
    
    // 同時借用多個
    fn get_two_tasks_mut(&mut self, i: usize, j: usize) -> Option<(&mut Task, &mut Task)> {
        if i == j {
            return None;  // 不能借用同一個任務兩次
        }
        
        // 分割可變借用
        if i < j {
            let (left, right) = self.tasks.split_at_mut(j);
            Some((&mut left[i], &mut right[0]))
        } else {
            let (left, right) = self.tasks.split_at_mut(i);
            Some((&mut right[0], &mut left[j]))
        }
    }
}

常見錯誤與解決方案

錯誤 1:懸垂參考(Dangling References)

// 這不會編譯!
fn dangle() -> &String {
    let s = String::from("hello");
    &s  // s 離開作用域會被丟棄,參考會懸垂
}

// 正確的做法:返回擁有的值
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 移動所有權
}

錯誤 2:參考的生命週期不夠長

fn main() {
    let r;
    {
        let x = 5;
        r = &x;  // 錯誤!x 的生命週期不夠長
    }
    // println!("r: {}", r);  // x 已經被丟棄
}

// 正確的做法
fn main() {
    let x = 5;
    let r = &x;
    println!("r: {}", r);  // x 仍然有效
}

實用技巧

1. 參數傳遞的最佳實踐

// 優先使用 &str 而不是 &String
fn process_text(text: &str) {
    println!("處理: {}", text);
}

// 優先使用 &[T] 而不是 &Vec<T>
fn process_numbers(numbers: &[i32]) {
    for &n in numbers {
        println!("{}", n);
    }
}

fn main() {
    let s = String::from("hello");
    process_text(&s);  // String 可以自動轉換為 &str
    
    let v = vec![1, 2, 3];
    process_numbers(&v);  // Vec<T> 可以自動轉換為 &[T]
}

2. 方法接收器的選擇

impl Task {
    // 使用 &self - 只讀取
    fn get_info(&self) -> String {
        format!("{}: {}", self.title, self.description)
    }
    
    // 使用 &mut self - 需要修改
    fn update(&mut self, title: String) {
        self.title = title;
    }
    
    // 使用 self - 需要消耗
    fn into_completed(mut self) -> Self {
        self.completed = true;
        self
    }
}

Follow-up

  1. 基礎練習:實作一個函式,接收字串參考並返回最長的單詞
fn find_longest_word(text: &str) -> &str {
    // 實作這個函式
    // 提示:使用 split_whitespace() 和迭代器
}
  1. 進階練習:實作一個可以同時修改多個元素的函式
fn swap_tasks(tasks: &mut Vec<Task>, i: usize, j: usize) {
    // 交換兩個任務的位置
    // 注意處理邊界情況
}
  1. 挑戰練習:建立一個借用檢查器
struct BorrowChecker<'a> {
    data: &'a str,
}

impl<'a> BorrowChecker<'a> {
    fn new(data: &'a str) -> Self {
        // 實作
    }
    
    fn get_data(&self) -> &str {
        // 實作
    }
}

明日預告

明天我們將學習生命週期(Lifetimes),這是 Rust 確保參考總是有效的機制。

今天的內容可能需要多練習幾次才能完全掌握,這很正常!借用規則一開始看起來很嚴格,但它們是 Rust 能夠保證記憶體安全的 secret sauce !


上一篇
Day 8: 理解 Rust 所有權系統與記憶體管理 (更新)
下一篇
Day 10: 生命週期:參考的有效性保證
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言